{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 0
   },
   "source": [
    "# 自动并行\n",
    ":label:`sec_auto_para`\n",
    "\n",
    "MXNet在后端自动构造计算图。使用计算图，系统知道所有的依赖关系，并且可以选择性地并行执行多个不相互依赖的任务提高速度。例如：numref:`fig_asyncgraph`in:numref:`sec_async`独立初始化两个变量。因此，系统可以选择并行执行它们。\n",
    "\n",
    "通常，单个操作员将使用所有CPU或单个GPU上的所有计算资源。例如，`dot`操作符将使用所有CPU上的所有核心（和线程），即使在一台机器上有多个CPU处理器。这同样适用于单个GPU。因此，并行化并不是很有用的单设备计算机。对于多个设备，事情更重要。虽然并行化通常在多个gpu之间最相关，但是添加本地CPU将略微提高性能。参见例如：引用：`哈吉斯.张.米利亚卡.ea.2016年的一篇论文，重点是训练结合了GPU和CPU的计算机视觉模型。利用自动并行化框架的便利性，我们可以用几行Python代码来实现相同的目标。更广泛地说，我们对自动并行计算的讨论集中在使用cpu和gpu的并行计算以及计算和通信的并行化。\n",
    "\n",
    "我们首先导入所需的包和模块。请注意，我们至少需要一个GPU来运行本节中的实验。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load ../utils/djl-imports\n",
    "%load ../utils/StopWatch.java"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 2
   },
   "source": [
    "## CPU与GPU的并行计算\n",
    "\n",
    "让我们通过选择下面的一个数据矩阵来执行分配给cpu的两个变量的乘法运算。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 3,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "public NDArray run(NDArray X){\n",
    "    \n",
    "    for(int i=0; i < 10; i++){\n",
    "        X = X.dot(X);\n",
    "    }\n",
    "    return X;\n",
    "}\n",
    "\n",
    "NDManager manager = NDManager.newBaseManager();\n",
    "NDArray x_cpu = manager.randomUniform(0f, 1f, new Shape(2000, 2000), DataType.FLOAT32, Device.cpu());\n",
    "NDArray x_gpu = manager.randomUniform(0f, 1f, new Shape(6000, 6000), DataType.FLOAT32, Device.gpu());"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 4
   },
   "source": [
    "现在我们将函数应用于数据。为了确保缓存不会在结果中起作用，我们在测量之前对每个设备执行一次循环来预热设备。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 5,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "// 设备初始化预热\n",
    "run(x_cpu);\n",
    "run(x_gpu);\n",
    "\n",
    "// 计算CPU计算时间\n",
    "StopWatch stopWatch0 = new StopWatch();\n",
    "stopWatch0.start();\n",
    "\n",
    "run(x_cpu);\n",
    "\n",
    "stopWatch0.stop();\n",
    "ArrayList<Double> times = stopWatch0.getTimes();\n",
    "System.out.println(\"CPU time: \" + times.get(times.size() - 1) + \" nanoseconds \");\n",
    "\n",
    "// 计算GPU计算时间\n",
    "StopWatch stopWatch1 = new StopWatch();\n",
    "stopWatch1.start();\n",
    "\n",
    "run(x_gpu);\n",
    "\n",
    "stopWatch1.stop();\n",
    "times = stopWatch1.getTimes();\n",
    "System.out.println(\"GPU time: \" + times.get(times.size() - 1) + \" nanoseconds \");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 7,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "// 计算CPU和GPU的组合计算时间\n",
    "StopWatch stopWatch = new StopWatch();\n",
    "stopWatch.start();\n",
    "\n",
    "run(x_cpu);\n",
    "run(x_gpu);\n",
    "\n",
    "stopWatch.stop();\n",
    "times = stopWatch.getTimes();\n",
    "System.out.println(\"CPU & GPU: \" + times.get(times.size() - 1) + \" nanoseconds \");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 8
   },
   "source": [
    "在上述情况下，总执行时间小于各部分的总和，因为MXNet自动地在CPU和GPU设备上调度计算，而不需要用户编写复杂的代码。 \n",
    "\n",
    "## 并行计算与通信\n",
    "\n",
    "在许多情况下，我们需要在不同的设备之间移动数据，比如在CPU和GPU之间，或者在不同的GPU之间。例如，当我们要执行分布式优化时，我们需要在多个加速卡上聚合渐变。让我们通过在GPU上计算然后将结果复制回CPU来模拟这一点。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 9,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "public NDArray copyToCPU(NDArray X){\n",
    "    Y = X.toDevice(Device.cpu(), true);\n",
    "    return Y;\n",
    "}\n",
    "\n",
    "// 计算GPU计算时间\n",
    "StopWatch stopWatch = new StopWatch();\n",
    "stopWatch.start();\n",
    "\n",
    "NDArray Y = run(x_gpu);\n",
    "\n",
    "stopWatch.stop();\n",
    "times = stopWatch.getTimes();\n",
    "System.out.println(\"Run on GPU: \" + times.get(times.size() - 1) + \" nanoseconds \");\n",
    "\n",
    "// 计算复制到CPU的时间\n",
    "StopWatch stopWatch1 = new StopWatch();\n",
    "stopWatch1.start();\n",
    "\n",
    "NDArray y_cpu = copyToCPU(Y);\n",
    "\n",
    "stopWatch1.stop();\n",
    "times = stopWatch1.getTimes();\n",
    "System.out.println(\"Copy to CPU: \" + times.get(times.size() - 1) + \" nanoseconds \");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 10
   },
   "source": [
    "这有点低效。注意，当列表的其余部分仍在计算时，我们可以开始将“Y”的一部分复制到CPU。例如，当我们计算一个小批量的（backprop）梯度时，就会出现这种情况。一些参数的梯度将比其他参数更早可用。因此，当GPU仍在运行时，开始使用PCI-Express总线带宽对我们是有利的。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "origin_pos": 11,
    "tab": [
     "mxnet"
    ]
   },
   "outputs": [],
   "source": [
    "// 计算组合GPU计算和复制到CPU时间。\n",
    "StopWatch stopWatch = new StopWatch();\n",
    "stopWatch.start();\n",
    "\n",
    "NDArray Y = run(x_gpu);\n",
    "NDArray y_cpu = copyToCPU(Y);\n",
    "\n",
    "stopWatch.stop();\n",
    "times = stopWatch.getTimes();\n",
    "System.out.println(\"Run on GPU and copy to CPU: \" + times.get(times.size() - 1) + \" nanoseconds \");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "origin_pos": 12
   },
   "source": [
    "两种操作所需的总时间（如预期）大大少于它们各部分的总和。注意，这个任务不同于并行计算，因为它使用不同的资源：CPU和GPU之间的总线。事实上，我们可以同时在两个设备上进行计算和通信。如上所述，在计算和通信之间有一个依赖关系：必须先计算Y[i]，然后才能将其复制到CPU。幸运的是，系统可以在计算Y[i]的同时复制Y[i-1]，以减少总的运行时间。\n",
    "\n",
    "最后，我们给出了一个简单的两层MLP在一个CPU和两个GPU上训练时的计算图及其依赖关系，如：numref:`fig_twogpu`。手动调度由此产生的并行程序将非常痛苦。这是一个有利于优化的基于图形的计算后端的地方。\n",
    "\n",
    "![CPU和2个GPU上的两层MLP](https://raw.githubusercontent.com/d2l-ai/d2l-en/8884afa3d1d3f6acd40fcc3aea0a3cf288461989/img/twogpu.svg)\n",
    ":label:`fig_twogpu`\n",
    "\n",
    "## 总结\n",
    "\n",
    "* 现代系统有各种各样的设备，比如多个gpu和cpu。它们可以并行、异步地使用。 \n",
    "* 现代系统也有各种各样的通信资源，如PCI-Express、存储器（通常是SSD或通过网络）和网络带宽。它们可以并行使用，以达到最高效率。\n",
    "* 后端通过自动并行计算和通信来提高性能。\n",
    "\n",
    "## 练习\n",
    "\n",
    "1. 在本节定义的“run”函数中执行了10个操作。它们之间没有依赖关系。设计一个实验，看看MXNet是否会自动并行执行它们。\n",
    "1. 当单个操作员的工作负载足够小时，即使在单个CPU或GPU上，并行化也有帮助。设计一个实验来验证这一点。 \n",
    "1. 设计了一个在CPU、GPU和两个设备之间进行并行计算的实验。\n",
    "1. 使用调试器（如NVIDIA的Nsight）验证代码是否有效。\n",
    "1. 设计包含更复杂的数据依赖关系的计算任务，并运行实验，看看是否可以在提高性能的同时获得正确的结果。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Java",
   "language": "java",
   "name": "java"
  },
  "language_info": {
   "codemirror_mode": "java",
   "file_extension": ".jshell",
   "mimetype": "text/x-java-source",
   "name": "Java",
   "pygments_lexer": "java",
   "version": "14.0.2+12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
